/**
 *  Serponix is an arcade game in focus to multiplayer based on the classic game Snake.
 *  Copyright (C) 2010 - 2011  Daniel Vala
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License,
 *  or  any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.

 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *  If you have any question do not hesitate to contact author
 *  on e-mail address: danielvala42@gmail.com
 */
package com.serponix.game;

import com.serponix.game.objektyNaPlose.VecNaPolicku;
import com.serponix.game.food.TypPotraviny;
import com.serponix.gui.WindowInterface;
import com.serponix.game.food.Cherry;
import com.serponix.game.food.Cake;
import com.serponix.game.score.Score;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.util.List;
import java.util.ArrayList;
import java.util.Observable;
import java.util.Random;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import com.serponix.game.food.Food;
import com.serponix.game.objektyNaPlose.ViceObjektu;
import com.serponix.game.projectiles.Projectile;
import java.awt.Graphics2D;
import java.awt.Point;

import javax.swing.Timer;
import com.serponix.settings.SettingsFactory;
import com.serponix.net.Informace;
import com.serponix.net.Net;

/**
 * Model celé hry. Obsahuje všechny hady, potraviny a objekty ve hře.
 * Má dvourozměrné pole všech políček ve hře, kam lze uložit věc na ploše, nebo ji z ní odebrat. Lze se také zeptat, na jakém políčku co leží.
 * Poskytuje metody pro krok hry, která by měla pohybovat hady a všemi objekty, které to vyžadují.
 * @author Daniel Vala
 */
public abstract class GameModel extends Observable {

    private WindowInterface okno;
    protected Net net;
    private Timer timer;
    private Uloha uloha;
    private boolean pauza;
    private Mapa mapa;
    public Cake jidlo;
    public Cherry bonus;
    protected Snake[] hadi;
    private List<Player> hraci;
    private int pocetHadu;
    private JLabel[] skoreStitky;
    private MessagePanel panelZprav;
    private GamingArea herniPanel;
    private int sirkaHernihoPaneluLogicka;
    private int vyskaHernihoPaneluLogicka;
    private int sirkaHernihoPaneluProVzhled;
    private int vyskaHernihoPaneluProVzhled;
    protected final Random generatorNahody;
    protected List<Integer> listPocatecnichPozic;
    private VecNaPolicku[][] veciNaPlose;
    private static final int velikostPolicka;
    private int casStitku = 0;
    protected Color[] barvy = {Color.GREEN, Color.BLUE, Color.RED, Color.YELLOW, Color.ORANGE, Color.MAGENTA, Color.PINK, Color.BLACK, Color.CYAN, Color.LIGHT_GRAY};
    public boolean ladeni = true;
    public boolean zobrazVeciNaPlose;
    public boolean krokovani;
    protected List<Projectile> strely;
    protected boolean newRound;

    static {
        velikostPolicka = SettingsFactory.velikostPolicka;
    }

    public GameModel(WindowInterface okno, GameParameters parametryHry, GamingArea herniPanel, JLabel[] skoreStitky, MessagePanel panelZprav, Dimension velikostHernihoPanelu)
            throws IllegalArgumentException {
        if (velikostHernihoPanelu.getWidth() < 100) {
            throw new IllegalArgumentException("Šírka herního panelu nesmí být menší než 100.");
        }
        this.okno = okno;
        this.herniPanel = herniPanel;
        this.skoreStitky = skoreStitky;
        this.pocetHadu = parametryHry.getNumberOfPlayers();
        this.hraci = parametryHry.getPlayers();
        sirkaHernihoPaneluProVzhled = (int) velikostHernihoPanelu.getWidth();
        vyskaHernihoPaneluProVzhled = (int) velikostHernihoPanelu.getHeight();
        this.panelZprav = panelZprav;
        this.mapa = parametryHry.getMap();
        generatorNahody = new Random();
        sirkaHernihoPaneluLogicka = sirkaHernihoPaneluProVzhled / getVelikostPolicka();
        vyskaHernihoPaneluLogicka = vyskaHernihoPaneluProVzhled / getVelikostPolicka();
        veciNaPlose = new VecNaPolicku[sirkaHernihoPaneluProVzhled][vyskaHernihoPaneluLogicka];
        mapa.namapujCelouMapu(this);
        strely = new ArrayList<Projectile>();

        // pokud je tento model modelem client, nevytvoří se timer
        if (!parametryHry.getMode().equals(Mode.CLIENT)) {
            uloha = new Uloha(this);
            timer = new Timer(parametryHry.getSpeed(), uloha);

        }
    }

    public void addStrela(Projectile strela) {
        strely.add(strela);
    }

    protected void odstranVsechnyStrely() {
        for (Projectile strela : strely) {
            strela.odmapuj(this);
        }
        strely.clear();

    }

    /**
     *
     * Definuje způsob vykreslení všech zdí mapy.
     * @param g Grafický objekt pro kreslení.
     */
    abstract void paintZdi(Graphics g);

    /**
     * Definuje, jak se bude resetovat zadaný had.
     * @param had Had, který bude resetován.
     */
    abstract void resetHada(Snake had);

    /**
     * Definuje, jak nastavit všechny hady do počáteční pozice.
     */
    public abstract void newRound();

    abstract void jidloSezrano(Snake had);

    abstract void bonusSezran(Snake had);

    /**
     * Definuje, co se stane za jeden krok hry.
     */
    abstract void krok();

    /**
     * Definuje, co se vykreslí na hrací plochu.
     * @param g
     */
    abstract void paint(Graphics g);

    /**
     * Definuje, co se stane při přidání daného počtu bodů do zadaného skóre.
     * @param skore Skóre, kam se přidají nové body.
     * @param body Počet bodů, které se přidají.
     */
    abstract void addScore(Score skore, int body);

    /**
     * Odstartuje hru zapnutím timeru na zadanou rychlost z parametrů hry.
     */
    protected void startHry() {
        timer.start();
    }

    public Point getPolickoPred(int x, int y, Direction smer) {
        switch (smer) {
            case UP:
                y--;
                break;
            case DOWN:
                y++;
                break;
            case LEFT:
                x--;
                break;
            case RIGHT:
                x++;
                break;
        }
        return new Point(x, y);
    }

    public void umistiVecNaHerniPlochu(VecNaPolicku novaVecNaPlose, int x, int y) {
        if (isVHerniMape(x, y)) {
            VecNaPolicku staraVecNaPlose = veciNaPlose[x][y];
            // pokud na požadovaném místě leží už jiná věc na ploše, nová se jen přidá
            if (staraVecNaPlose != null) {
                // pokud na požadovaném místě leží více věcí na ploše, tato se přidá
                if (staraVecNaPlose instanceof ViceObjektu) {
                    ViceObjektu viceObjektu = (ViceObjektu) staraVecNaPlose;
                    viceObjektu.addVec(novaVecNaPlose);
                    // pokud na požadovaném místě leží právě jedna věc na ploše,
                    // vytvoří se instance více objektů, kde se předá stará i nová věc
                } else {
                    ViceObjektu viceObjektu = new ViceObjektu(staraVecNaPlose, novaVecNaPlose);
                    veciNaPlose[x][y] = viceObjektu;
                }
                // pokud na požadovaném místě neleží žádná věc na ploše, jednoduše se tam přidá nová věc na ploše
            } else {
                veciNaPlose[x][y] = novaVecNaPlose;
            }
        }
    }

    public boolean odstranVecNaHerniPlose(VecNaPolicku vecNaPloseKOdstraneni) {
        int x = vecNaPloseKOdstraneni.getX();
        int y = vecNaPloseKOdstraneni.getY();
        // pokud chci odmapovávat v herní ploše a něco je zde skutečně namapováno
        if (isVHerniMape(x, y) && veciNaPlose[x][y] != null) {
            // pokud je na místě, kde je věc na odmapování, ještě jiná věc, odmapuje se jen požadovaná
            if (veciNaPlose[x][y] instanceof ViceObjektu) {
                ViceObjektu viceObjektu = (ViceObjektu) veciNaPlose[x][y];
                boolean removed = viceObjektu.removeVec(vecNaPloseKOdstraneni);
                // pokud po odmapování požadované věci zbývá na souřadnicích už jen jedna věc,
                // zruší se instance více objektů a do modelu se předá jen tato jedna věc
                if (viceObjektu.getPocetVeci() == 1) {
                    veciNaPlose[x][y] = viceObjektu.getVecNaPlose(0);
                }
                return removed;
                // pokud je na daném místě jen jedna věc a to ta, kterou chci odmapovat
            } else if (vecNaPloseKOdstraneni.equals(veciNaPlose[x][y])) {
                veciNaPlose[x][y] = null;
                return true;
            }
        }
        return false;
    }

    public VecNaPolicku getObjektNaHerniPlose(int x, int y) {
        if (isVHerniMape(x, y)) {
            return veciNaPlose[x][y];
        } else {
            return null;
        }
    }

    public void zobrazVeciNaPlose(Graphics g) {
        Graphics2D g2d = (Graphics2D) g;
        for (int i = 0; i < veciNaPlose.length; i++) {
            for (int j = 0; j < veciNaPlose[i].length; j++) {
                if (veciNaPlose[i][j] != null) {
                    if (veciNaPlose[i][j] instanceof ViceObjektu) {
                        g2d.setPaint(Color.RED);
                    } else {
                        g2d.setPaint(Color.BLUE);
                    }
                    g2d.fillOval(i * velikostPolicka, j * velikostPolicka, velikostPolicka, velikostPolicka);
                }
            }
        }
    }

    public boolean isVHerniMape(int x, int y) {
        if (x >= 0 && y >= 0 && x < getSirkaLogicka() && y < getVyskaLogicka()) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Pošle zadaná data všem klientům.
     * @param data Data k poslání.
     */
    public void posliDataKlientum(String data) {
        net.poslatData(data);
    }

    public Cake getJidlo() {
        return jidlo;
    }

    public Cherry getBonus() {
        return bonus;
    }

    public Food getPotravina(TypPotraviny typPotraviny) {
        if (typPotraviny == TypPotraviny.JIDLO) {
            return jidlo;
        } else if (typPotraviny == TypPotraviny.BONUS) {
            return bonus;
        } else {
            return null;
        }
    }

    /**
     * Ukončí hru a pošle všem klientům tuto informaci.
     * @param zpravaOVitezstvi Zpráva, kterou model zobrazí a kterou pošle klientům.
     */
    public void ukonciHruVitezstvim(String zpravaOVitezstvi) {
        net.poslatData(Informace.VITEZSTVI + Informace.ODDELOVAC1 + zpravaOVitezstvi);
        JOptionPane.showMessageDialog(null, zpravaOVitezstvi, "KONEC HRY", JOptionPane.INFORMATION_MESSAGE);
        ukonciHru();
    }

    /**
     * Ukončí hru a zobrazí menu.
     */
    public void ukonciHru() {
        if (net != null) {
            net.odpojSe();
        }
        zastavHru();
        okno.displayMenu();
    }

    /**
     * Vrátí mapu hry.
     * @return Mapa
     */
    public Mapa getMapa() {
        return mapa;
    }

    public int getSirkaLogicka() {
        return sirkaHernihoPaneluLogicka;
    }

    public int getVyskaLogicka() {
        return vyskaHernihoPaneluLogicka;
    }

    /**
     * Vrátí šířku herního panelu.
     * @return Šířka herního panelu.
     */
    public int getSirkaProVzhled() {
        return sirkaHernihoPaneluProVzhled;
    }

    /**
     * Vrátí výšku herního panelu.
     * @return Výška herního panelu.
     */
    public int getVyskaProVzhled() {
        return vyskaHernihoPaneluProVzhled;
    }

    /**
     * Vrátí celkový počet hadů, živých i mrtvých.
     * @return Počet všech hadů.
     */
    public int getPocetHadu() {
        return pocetHadu;
    }

    /**
     * Vrátí velikost políčka - čtverečku.
     * @return Rozměr strany čtvercového políčka herní plochy.
     */
    public static int getVelikostPolicka() {
        return velikostPolicka;
    }

    public Player getHrac(int cisloHrace) {
        return hraci.get(cisloHrace);
    }

    public String getSkoreText(int cisloStitku) {
        return skoreStitky[cisloStitku].getText();
    }

    /**
     * Nastaví zadaný text na daný štítek.
     * Pokud je šítků více než 5, nemuselo by být pro všechny místo,
     * proto se zobrazí pouze sudé nebo liché štítky v závislosti na čase.
     * Čas je závislý na rychlosti hry.
     * @param cisloStitku Číslo šítku, který se nastavý na daný text.
     * @param text Text, který se nastaví na zadaný štítek.
     */
    public void setSkoreText(int cisloStitku, String text) {
        if (pocetHadu <= 5) {
            skoreStitky[cisloStitku].setText(text);
        } else {
            if (casStitku < 900) {
                if (cisloStitku % 2 == 0) {
                    skoreStitky[cisloStitku].setText(text);
                } else {
                    skoreStitky[cisloStitku].setText("");
                }
            } else {
                if (cisloStitku % 2 == 1) {
                    skoreStitky[cisloStitku].setText(text);
                } else {
                    skoreStitky[cisloStitku].setText("");
                }
            }
            casStitku++;
            if (casStitku >= 1800) {
                casStitku = 0;
            }
        }
    }

    public void setAllSkoreText(int cisloStitku, String text) {
        skoreStitky[cisloStitku].setText(text);
    }

    public MessagePanel getPanelZprav() {
        return panelZprav;
    }

    public GamingArea getHerniPanel() {
        return herniPanel;
    }

    protected void vytvorListPocatecnichPozic(int pocetPozic) {
        if (getSirkaLogicka() < 10) {
            throw new RuntimeException("Logická šířka herního panelu nesmí být menší než 10.");
        }
        listPocatecnichPozic = new ArrayList<Integer>(10);
        int zaklad = getSirkaLogicka() / pocetPozic;
        for (int i = 0; i < pocetPozic; i++) {
            listPocatecnichPozic.add(zaklad * i);
        }
    }

    /**
     * Pokud zbyl už jen jediný had naživu, stane se vítězem.
     * @return <tt> true </tt> pokud se jediný zbylý had stal vítězem.
     */
    protected boolean isWinner() {
        Snake posledni = null;
        for (int i = 0; i < getPocetHadu(); i++) {
            if (hadi[i].isAlive()) {
                if (posledni == null) {
                    posledni = hadi[i];
                } else {
                    return false; // pokud se najde druhý živý had, nic se nestane
                }
            }
        }
        if (posledni != null) {
            posledni.score.vyhra();
            return true;
        } else {
            System.err.println("CHYBA! Všichni hadi jsou mrtví");
            return false;
        }
    }

    /**
     * Zjistí, zda je ve hře živý hráč, nebo hrají už jen umělé inteligence.
     * @return True, pokud hraje živý hráč. False, pokud hrají jen AI.
     */
    public boolean isHumanPlaying() {
        for (Player hrac : hraci) {
            if (hrac.getAI() == null && hrac.getSnake().isAlive()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Oživne všechny hady.
     */
    public void ozivVsechnyHady() {
        for (int i = 0; i < getPocetHadu(); i++) {
            hadi[i].oziv();
        }
    }

    /**
     * Zkontroluje, pokud je daný had blízko některé z krajních stěn.
     * @param had
     * @return směr, kterým by se had měl vydat, aby se stěně vyhnul (proti hodinovým ručičkám).
     */
    public Direction vyhniSeStenam(Snake had) {
        if (had.getX() < 20) {
            return Direction.LEFT;
        }

        if (had.getY() < 20) {
            return Direction.UP;
        }

        if (Math.abs(had.getX() - getSirkaProVzhled()) < 20) {
            return Direction.RIGHT;
        }

        if (Math.abs(had.getY() - getVyskaProVzhled()) < 20) {
            return Direction.RIGHT;
        }

        return null;
    }

    /**
     * Pozastaví hru na zadaný počet milisekund.
     * @param cas Čas v milisekundách.
     */
    public void pockej(int cas) {
        timer.setInitialDelay(cas);
        timer.restart();
        timer.setInitialDelay(0);
    }

    /**
     * Zastaví hru zrušením timeru, pokud existuje.
     */
    public void zastavHru() {
        if (timer != null) {
            timer.stop();
        }
    }

    /**
     * Opětovně spustí celou hru
     */
    public void pokracuj() {
        timer.start();
    }

    /**
     * Zastaví nebo opět spustí běh celé hry.
     */
    public void pauza() {
        if (!pauza) {
            zastavHru();
            pauza = true;
        } else {
            pokracuj();
            pauza = false;
        }
    }

//    public void zrychli() {
//        timer.cancel();
//        timer = new Timer();
//        uloha = new Uloha(this);
//        timer.scheduleAtFixedRate(uloha, 0, parametryHry.getRychlost() - 10);
//    }
//
//    public void zpomal() {
//        timer.cancel();
//        timer = new Timer();
//        uloha = new Uloha(this);
//        timer.scheduleAtFixedRate(uloha, 0, parametryHry.getRychlost() + 10);
//    }
    public int getPocetZivychHadu() {
        int ziviHadi = 0;
        for (int i = 0; i < getPocetHadu(); i++) {
            if (hadi[i].isAlive()) {
                ziviHadi++;
            }
        }
        return ziviHadi;
    }

    public List<Snake> getZiviHadi() {
        List<Snake> ziviHadi = new ArrayList<Snake>();
        for (int i = 0; i < getPocetHadu(); i++) {
            if (hadi[i].isAlive()) {
                ziviHadi.add(hadi[i]);
            }
        }
        return ziviHadi;
    }

    /**
     * Vrátí pole všech hadů ve hře.
     * @return Pole všech hadů.
     */
    public Snake[] getHadi() {
        Snake[] retval = new Snake[hraci.size()];
        for (int i = 0; i < retval.length; i++) {
            retval[i] = hraci.get(i).getSnake();
        }
        return retval;
    }
}
